/*******************************************************************************
 * Copyright (c) 2006, 2016 QNX Software Systems and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 * 	   QNX - Initial API and implementation
 *     Markus Schorn (Wind River Systems)
 *     Sergey Prigogin (Google)
 *******************************************************************************/
package org.eclipse.cdt.internal.core.pdom.dom;

import org.eclipse.cdt.core.dom.ast.IASTFileLocation;
import org.eclipse.cdt.core.dom.ast.IASTName;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorIncludeStatement;
import org.eclipse.cdt.core.index.IIndexFile;
import org.eclipse.cdt.core.index.IIndexFileLocation;
import org.eclipse.cdt.internal.core.index.IIndexFragment;
import org.eclipse.cdt.internal.core.index.IIndexFragmentFile;
import org.eclipse.cdt.internal.core.index.IIndexFragmentInclude;
import org.eclipse.cdt.internal.core.pdom.db.Database;
import org.eclipse.core.runtime.CoreException;

/**
 * @author Doug Schaefer
 */
public class PDOMInclude implements IIndexFragmentInclude {
	private static final int INCLUDED_FILE		 	=  0;
	private static final int INCLUDED_BY 			=  4;
	private static final int INCLUDES_NEXT 			=  8;
	private static final int INCLUDED_BY_NEXT 		= 12;
	private static final int INCLUDED_BY_PREV 		= 16;
	// If the include name is the same as the end part of the path of the included file,
	// we store the length of the name instead of the name itself, and indicate that
	// by turning on FLAG_DEDUCIBLE_NAME flag. Notice that the length of include name
	// can be different from the node length, if the name is defined by a macro. 
	private static final int INCLUDE_NAME_OR_LENGTH = 20; // TODO: assumes that int and stored pointers are the same size
	private static final int NODE_OFFSET  			= 24; // 3-byte unsigned int (sufficient for files <= 16mb)
	private static final int NODE_LENGTH  			= 27; // short (sufficient for names <= 32k)
	private static final int FLAGS		 			= 29;
	private static final int RECORD_SIZE 			= 30;

	private static final int FLAG_SYSTEM_INCLUDE 		= 0x01;
	private static final int FLAG_INACTIVE_INCLUDE 		= 0x02;
	private static final int FLAG_RESOLVED_BY_HEURISTICS= 0x04;
	private static final int FLAG_DEDUCIBLE_NAME		= 0x08;
	private static final int FLAG_EXPORTED_FILE			= 0x10;
	
	private final PDOMLinkage linkage;
	private final long record;

	// Cached fields
	private String fName;

	public PDOMInclude(PDOMLinkage linkage, long record) {
		this.linkage = linkage;
		this.record = record;
	}

	public PDOMInclude(PDOMLinkage linkage, IASTPreprocessorIncludeStatement include,
			PDOMFile containerFile, PDOMFile targetFile) throws CoreException {
		this.linkage = linkage;
		this.record = linkage.getDB().malloc(RECORD_SIZE);
		IASTName name = include.getName();
		char[] nameChars = name.getSimpleID();
		IASTFileLocation loc = name.getFileLocation();
		// Includes generated by -include or -macro don't have a location
		if (loc != null) {
			linkage.getDB().put3ByteUnsignedInt(record + NODE_OFFSET, loc.getNodeOffset());
			linkage.getDB().putShort(record + NODE_LENGTH, (short) loc.getNodeLength());
		}

		final Database db = linkage.getDB();
		if (targetFile != null) {
			db.putRecPtr(record + INCLUDED_FILE, targetFile.getRecord());
		}
		boolean deducible_name = isDeducibleName(targetFile, nameChars);
		// If the name is the same as an end part of the path of the included file,
		// store the length of the name instead of the name itself.
		if (deducible_name) {
			db.putInt(record + INCLUDE_NAME_OR_LENGTH, nameChars.length);
		} else {
			db.putRecPtr(record + INCLUDE_NAME_OR_LENGTH, db.newString(nameChars).getRecord());
		}
		setFlag(encodeFlags(include, deducible_name));
		setIncludedBy(containerFile);
	}

	private byte encodeFlags(IASTPreprocessorIncludeStatement include, boolean deducible_name) {
		byte flags= 0;
		if (include.isSystemInclude()) {
			flags |= FLAG_SYSTEM_INCLUDE;
		}
		if (!include.isActive()) {
			flags |= FLAG_INACTIVE_INCLUDE;
		} else if (include.isResolvedByHeuristics()) {
			flags |= FLAG_RESOLVED_BY_HEURISTICS;
		}
		if (include.isIncludedFileExported()) {
			flags |= FLAG_EXPORTED_FILE;
		}
		if (deducible_name) {
			flags |= FLAG_DEDUCIBLE_NAME;
		}
		return flags;
	}

	public long getRecord() {
		return record;
	}

	public void delete() throws CoreException {
		if (isResolved()) {
			// Remove us from the includedBy chain
			removeThisFromIncludedByChain();
		}
		final Database db = linkage.getDB();
		if ((getFlag() & FLAG_DEDUCIBLE_NAME) == 0) {
			long rec = db.getRecPtr(record + INCLUDE_NAME_OR_LENGTH);
			db.getString(rec).delete();
		}
		// Delete our record
		db.free(record);
	}

	private void removeThisFromIncludedByChain() throws CoreException {
		PDOMInclude prevInclude = getPrevInIncludedBy();
		PDOMInclude nextInclude = getNextInIncludedBy();
		if (prevInclude != null) {
			prevInclude.setNextInIncludedBy(nextInclude);
		} else {
			((PDOMFile) getIncludes()).setFirstIncludedBy(nextInclude);
		}

		if (nextInclude != null)
			nextInclude.setPrevInIncludedBy(prevInclude);
	}

	@Override
	public IIndexFragmentFile getIncludes() throws CoreException {
		long rec = linkage.getDB().getRecPtr(record + INCLUDED_FILE);
		return rec != 0 ? new PDOMFile(linkage, rec) : null;
	}

	void setIncludes(PDOMFile includedFile) throws CoreException {
		long rec = includedFile != null ? includedFile.getRecord() : 0;
		linkage.getDB().putRecPtr(record + INCLUDED_FILE, rec);
	}

	/**
	 * Checks if the name is the same as the end part of the path of the included file.
	 */
	private static boolean isDeducibleName(PDOMFile includedFile, char[] name) throws CoreException {
		if (includedFile == null) {
			return false;
		}
		String s = includedFile.getLocation().getURI().getPath();
		int pos = s.length() - name.length;
		if (pos < 0) {
			return false;
		}
		for (int i = 0; i < name.length; i++, pos++) {
			if (s.charAt(pos) != name[i]) {
				return false;
			}
		}
		return true;
	}

	@Override
	public IIndexFile getIncludedBy() throws CoreException {
		long rec = linkage.getDB().getRecPtr(record + INCLUDED_BY);
		return rec != 0 ? new PDOMFile(linkage, rec) : null;
	}

	void setIncludedBy(PDOMFile includedBy) throws CoreException {
		long rec = includedBy != null ? includedBy.getRecord() : 0;
		linkage.getDB().putRecPtr(record + INCLUDED_BY, rec);
	}

	public PDOMInclude getNextInIncludes() throws CoreException {
		long rec = linkage.getDB().getRecPtr(record + INCLUDES_NEXT);
		return rec != 0 ? new PDOMInclude(linkage, rec) : null;
	}

	public void setNextInIncludes(PDOMInclude include) throws CoreException {
		long rec = include != null ? include.getRecord() : 0;
		linkage.getDB().putRecPtr(record + INCLUDES_NEXT, rec);
	}

	public PDOMInclude getNextInIncludedBy() throws CoreException {
		long rec = linkage.getDB().getRecPtr(record + INCLUDED_BY_NEXT);
		return rec != 0 ? new PDOMInclude(linkage, rec) : null;
	}

	public void setNextInIncludedBy(PDOMInclude include) throws CoreException {
		long rec = include != null ? include.getRecord() : 0;
		linkage.getDB().putRecPtr(record + INCLUDED_BY_NEXT, rec);
	}

	public PDOMInclude getPrevInIncludedBy() throws CoreException {
		long rec = getPrevInIncludedByRecord();
		return rec != 0 ? new PDOMInclude(linkage, rec) : null;
	}

	long getPrevInIncludedByRecord() throws CoreException {
		return linkage.getDB().getRecPtr(record + INCLUDED_BY_PREV);
	}

	public void setPrevInIncludedBy(PDOMInclude include) throws CoreException {
		long rec = include != null ? include.getRecord() : 0;
		linkage.getDB().putRecPtr(record + INCLUDED_BY_PREV, rec);
	}

	@Override
	public IIndexFileLocation getIncludedByLocation() throws CoreException {
		return getIncludedBy().getLocation();
	}

	@Override
	public IIndexFileLocation getIncludesLocation() throws CoreException {
		IIndexFragmentFile includes = getIncludes();
		return includes != null ? includes.getLocation() : null;
	}

	@Override
	public IIndexFragment getFragment() {
		return linkage.getPDOM();
	}

	private void setFlag(byte flag) throws CoreException {
		linkage.getDB().putByte(record + FLAGS, flag);
	}

	private int getFlag() throws CoreException {
		return linkage.getDB().getByte(record + FLAGS);
	}

	@Override
	public boolean isSystemInclude() throws CoreException {
		return (getFlag() & FLAG_SYSTEM_INCLUDE) != 0;
	}

	@Override
	public boolean isActive() throws CoreException {
		return (getFlag() & FLAG_INACTIVE_INCLUDE) == 0;
	}

	@Override
	public boolean isResolved() throws CoreException {
		return linkage.getDB().getRecPtr(record + INCLUDED_FILE) != 0;
	}

	@Override
	public boolean isResolvedByHeuristics() throws CoreException {
		return (getFlag() & FLAG_RESOLVED_BY_HEURISTICS) != 0;
	}

	@Override
	public boolean isIncludedFileExported() throws CoreException {
		return (getFlag() & FLAG_EXPORTED_FILE) != 0;
	}
	
	@Override
	public int getNameOffset() throws CoreException {
		return linkage.getDB().get3ByteUnsignedInt(record + NODE_OFFSET);
	}

	@Override
	public int getNameLength() throws CoreException {
		return linkage.getDB().getShort(record + NODE_LENGTH) & 0xffff;
	}	

	@Override
	public String getFullName() throws CoreException {
		if (fName == null) {
			final Database db = linkage.getDB();
			// The include name is either stored explicitly, or can be deduced from the path
			// of the included file.
			if ((getFlag() & FLAG_DEDUCIBLE_NAME) == 0) {
				long rec = db.getRecPtr(record + INCLUDE_NAME_OR_LENGTH);
				fName = db.getString(rec).getString();
			} else {
				String path = getIncludes().getLocation().getURI().getPath();
				int nameLength = db.getInt(record + INCLUDE_NAME_OR_LENGTH);
				fName = path.substring(Math.max(path.length() - nameLength, 0));
			}
		}
		return fName;
	}

	@Override
	public String getName() throws CoreException {
		final String fullName= getFullName();
		final int idx= Math.max(fullName.lastIndexOf('/'), fullName.lastIndexOf('\\'));
		return fullName.substring(idx + 1);
	}

	public void convertToUnresolved() throws CoreException {
		if (isResolved()) {
			final Database db = linkage.getDB();
			int flag = getFlag();
			// Since included file is going away, the name is no longer deducible.
			if ((flag & FLAG_DEDUCIBLE_NAME) != 0) {
				long rec= db.newString(getFullName()).getRecord();
				db.putRecPtr(record + INCLUDE_NAME_OR_LENGTH, rec);
				setFlag((byte) (flag & ~FLAG_DEDUCIBLE_NAME));
			}
			db.putRecPtr(record + INCLUDED_FILE, 0);
		}
	}

	@Override
	public String toString() {
		StringBuilder buf = new StringBuilder();
		try {
			boolean isSystem = isSystemInclude();
			buf.append(isSystem ? '<' : '"');
			buf.append(getFullName());
			buf.append(isSystem ? '>' : '"');
			IIndexFile includedBy = getIncludedBy();
			if (includedBy != null)
				buf.append(" in ").append(includedBy); //$NON-NLS-1$
			IIndexFragmentFile includes = getIncludes();
			if (includes != null) {
				buf.append(" resolved to ").append(includes); //$NON-NLS-1$
			} else {
				buf.append(" unresolved"); //$NON-NLS-1$
			}
		} catch (CoreException e) {
			buf.append(" (incomplete due to ").append(e.getClass().getName()).append(')'); //$NON-NLS-1$
			e.printStackTrace();
		}
		return buf.toString();
	}
}
